<?php

namespace ASW\Communication;

use ASW\Communication\Rdc\RdcClientRequest;
use ASW\Communication\Rdc\RdcClientRequestBehavior;
use ASW\Communication\Rdc\RdcServerCommand;
use ASW\Communication\Rdc\RdcServerFrame;
use ASW\Communication\Rdc\RdcServerFrameType;
use ASW\Communication\Rdc\RdcServerResponse;
use ASW\Utility\ExecuteResult;
use Exception;
use Throwable;
use Workerman\Connection\AsyncTcpConnection;
use Workerman\Connection\TcpConnection;

/**
 * RDC 协议客户端
 */
class ApiClient
{
    /**
     * @var string 服务器默认地址
     */
    public static string $defaultServerHost = '127.0.0.1';

    /**
     * @var int 服务器默认端口
     */
    public static int $defaultServerPort = 2218;

    private AsyncTcpConnection $connection;
    private bool               $_isConnected        = false;
    private array              $responseCallbackDic = [];
    private array              $commandHandlerDic   = [];

    /**
     * @var ?callable 开始连接时的回调
     */
    private $onConnectingHandler = null;

    /**
     * @var ?callable 连接失败时的回调
     */
    private $onConnectFailHandler = null;

    /**
     * @var ?callable 连接成功时的回调
     */
    private $onConnectedHandler = null;

    /**
     * @var ?callable 连接发生异常时的回调
     */
    private $onErrorHandler = null;

    /**
     * @var ?callable 连接关闭时的回调
     */
    private $onClosedHandler = null;

    public function __construct(private string $serverHost = '', private int $serverPort = 0)
    {
        if (empty($this->serverHost)) $this->serverHost = self::$defaultServerHost;
        if ($this->serverPort <= 0) $this->serverPort = self::$defaultServerPort;

        if (empty($this->serverHost)) throw new Exception(__CLASS__ . ' 初始化失败: 提供的服务器地址为空');
        if ($this->serverPort <= 0 || $this->serverPort > 0xffff) throw new Exception(__CLASS__ . " 初始化失败: 提供的服务器端口 [$this->serverPort] 不可用");

        $this->connection            = new AsyncTcpConnection("rdc://$this->serverHost:$this->serverPort");
        $this->connection->onConnect = function (TcpConnection $connection) {
            $this->_isConnected = true;
            if (is_callable($this->onConnectedHandler)) {
                call_user_func($this->onConnectedHandler, $this);
            }
        };

        $this->connection->onClose = function (TcpConnection $connection) {
            if ($this->_isConnected) {
                // 如果当前连接状态为已连接, 表明是连接后断开
                $this->_isConnected = false;
                if (is_callable($this->onClosedHandler)) {
                    call_user_func($this->onClosedHandler, $this);
                }
            } else {
                // 否则说明是连接失败
                if (is_callable($this->onConnectFailHandler)) {
                    call_user_func($this->onConnectFailHandler, $this);
                }
            }

            if ($this->isAutoReconnect) {
                if (is_callable($this->onConnectingHandler)) {
                    call_user_func($this->onConnectingHandler, $this);
                }
                $this->connection->reconnect($this->autoReconnectDelay / 1000);
            }
        };

        $this->connection->onMessage = function (TcpConnection $connection, ?RdcServerFrame $serverFrame) {
            if ($serverFrame == null) return;
            if ($serverFrame->type == RdcServerFrameType::Response) {
                /**
                 * @type RdcServerResponse $serverResponse
                 */
                $serverResponse = $serverFrame;
                $this->applyServerResponse($serverResponse);
            } else if ($serverFrame->type == RdcServerFrameType::Command) {
                /**
                 * @type RdcServerCommand $serverCommand
                 */
                $serverCommand       = $serverFrame;
                $commandHandleResult = $this->applyServerCommand($serverCommand);
                $replyRequest        = $serverCommand->makeReplyRequest($commandHandleResult);
                $connection->send($replyRequest);
            }
        };

        $this->connection->onError = function (TcpConnection $connection, int $code, string $reason) {
            if (is_callable($this->onErrorHandler)) {
                call_user_func($this->onErrorHandler, $this, $code, $reason);
            }
        };
    }

    /**
     * @var bool 是否已启用自动重连
     */
    public bool $isAutoReconnect = false;

    /**
     * @var int 自动重连前的等待时间
     */
    public int $autoReconnectDelay = 2000;

    /**
     * @return string 获取服务器连接地址
     */
    public function getServerAddress(): string
    {
        return $this->connection->getRemoteAddress();
    }

    /**
     * @return string 获取服务器主机名
     */
    public function getServerHost(): string
    {
        return $this->serverHost;
    }

    /**
     * @return int 获取服务器端口
     */
    public function getServerPort(): int
    {
        return $this->serverPort;
    }

    /**
     * @return bool 当前是否已连接
     */
    public function isConnected(): bool
    {
        return $this->_isConnected;
    }

    /**
     * 开始连接
     * @return void
     * @throws Throwable
     */
    public function connect(): void
    {
        if (is_callable($this->onConnectingHandler)) {
            call_user_func($this->onConnectingHandler, $this);
        }
        $this->connection->connect();
    }

    /**
     * 关闭连接并停止自动重连
     * @return void
     * @throws Throwable
     */
    public function close(): void
    {
        $this->isAutoReconnect = false;
        $this->connection->close();
    }

    /**
     * 设置开始连接时的回调
     * @param callable $handler
     * @return void
     */
    public function onConnecting(callable $handler): void
    {
        $this->onConnectingHandler = $handler;
    }

    /**
     * 设置连接失败时的回调
     * @param callable $handler
     * @return void
     */
    public function onConnectFail(callable $handler): void
    {
        $this->onConnectFailHandler = $handler;
    }

    /**
     * 设置连接成功时的回调
     * @param callable $handler
     * @return void
     */
    public function onConnected(callable $handler): void
    {
        $this->onConnectedHandler = $handler;
    }

    /**
     * 设置连接发生异常时的回调
     * @param callable $handler
     * @return void
     */
    public function onError(callable $handler): void
    {
        $this->onErrorHandler = $handler;
    }

    /**
     * 设置连接已断开时的回调
     * @param callable $handler
     * @return void
     */
    public function onClosed(callable $handler): void
    {
        $this->onClosedHandler = $handler;
    }

    /**
     * 设置断开时是否自动重连
     * @param bool $enabled 断开时是否自动重连
     * @param int $delayMs 重连前延时, 单位毫秒
     * @return void
     */
    public function setAutoReconnect(bool $enabled, int $delayMs = 2000): void
    {
        $this->isAutoReconnect    = $enabled;
        $this->autoReconnectDelay = $delayMs;
    }

    /**
     * 发送请求
     * @param string $action
     * @param array|null $args
     * @param callable|null $callback fn(ExecuteResult) => void
     * @return void
     * @throws Throwable
     */
    public function send(string $action, ?array $args = null, ?callable $callback = null): void
    {
        $request = RdcClientRequest::create($action, $args);
        $this->sendRequest($request, $callback);
    }

    private function socketLastFailToExecuteResult(\Socket $socket, string $failPrefix): ExecuteResult
    {
        // 获取socket错误的文本描述
        $errorCode = socket_last_error($socket);
        $errorText = socket_strerror($errorCode);
        socket_clear_error($socket);
        return ExecuteResult::fail("$failPrefix ($errorCode) $errorText");
    }

    /**
     * 同步直接发送请求 (普通网站后台用)
     * @param string $action
     * @param array|null $args
     * @param bool $noResponse
     * @return ExecuteResult 发送请求的结果
     */
    public function sendSync(string $action, ?array $args = null, bool $noResponse = false): ExecuteResult
    {
        $client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if (!socket_connect($client, $this->serverHost, $this->serverPort)) {
            return $this->socketLastFailToExecuteResult($client, "连接到服务端失败");
        }

        $request = RdcClientRequest::create($action, $args);
        if ($noResponse) {
            $request->setBehavior(RdcClientRequestBehavior::NO_RESPONSE);
        }
        $request->setBehavior(RdcClientRequestBehavior::CLOSE_ME);
        $requestContent = json_encode($request, JSON_UNESCAPED_UNICODE);
        $totalLength    = 4 + strlen($requestContent);
        $data           = pack('N', $totalLength) . $requestContent;
        $sendResult     = socket_write($client, $data);
        if ($sendResult === false) {
            return $this->socketLastFailToExecuteResult($client, "发送数据失败");
        }

        if ($sendResult === 0) {
            return $this->socketLastFailToExecuteResult($client, "发送数据失败");
        }

        if ($noResponse) {
            socket_close($client);
            return ExecuteResult::success();
        }

        $totalLengthBuffer = socket_read($client, 4);
        if ($totalLengthBuffer === false) {
            return $this->socketLastFailToExecuteResult($client, "读取服务端返回数据失败");
        }

        if (empty($totalLengthBuffer)) {
            return ExecuteResult::fail("服务端没有返回任何数据");
        }

        $unpackArray   = unpack('Ntotal_length', $totalLengthBuffer);
        $totalLength   = $unpackArray['total_length'];
        $contentLength = $totalLength - 4;
        if (false === $responseContent = socket_read($client, $contentLength)) {
            return $this->socketLastFailToExecuteResult($client, "服务端返回声明内容长度为 $contentLength, 但是读取失败");
        }
        $array    = json_decode($responseContent, true);
        $response = RdcServerResponse::fromArray($array);

        return $response->result;
    }

    /**
     * 同步直接发送请求, 并忽略服务端返回的响应 (普通网站后台用)
     * @param string $action
     * @param array|null $args
     * @return ExecuteResult 发送请求的结果
     */
    public function sendSyncNoResponse(string $action, ?array $args = null): ExecuteResult
    {
        return $this->sendSync($action, $args, true);
    }

    /**
     * 发送请求
     * @param RdcClientRequest $request
     * @param callable|null $callback fn(ExecuteResult) => void
     * @return void
     * @throws Throwable
     */
    public function sendRequest(RdcClientRequest $request, ?callable $callback = null): void
    {
        if (!$this->_isConnected) {
            if (is_callable($callback)) $callback(ExecuteResult::fail("server not connected"));
            return;
        }
        $this->connection->send($request);
        // echo "after send [$request->seq]\n";
        if (is_callable($callback)) $this->responseCallbackDic[$request->seq] = $callback;
        // echo "callbacks: " . json_encode(array_keys($this->responseCallbackDic)) . "\n";
    }

    /**
     * 设置服务端命令处理器
     * @param string $commandAction 可以处理的服务端命令
     * @param callable $handler 命令处理器 fn(string $commandAction, array $commandArgs) => ExecuteResult
     * @return void
     */
    public function onCommand(string $commandAction, callable $handler): void
    {
        $this->commandHandlerDic[$commandAction] = $handler;
    }

    private function applyServerResponse(RdcServerResponse $serverResponse): void
    {
        if (!array_key_exists($serverResponse->seq, $this->responseCallbackDic)) return;
        $callback = $this->responseCallbackDic[$serverResponse->seq];
        $callback($serverResponse->result);
        unset($this->responseCallbackDic[$serverResponse->seq]);
    }

    private function applyServerCommand(RdcServerCommand $serverCommand): ExecuteResult
    {
        if (array_key_exists($serverCommand->action, $this->commandHandlerDic)) {
            $handler      = $this->commandHandlerDic[$serverCommand->action];
            $handleResult = $handler($serverCommand->action, $serverCommand->args);
            if ($handleResult instanceof ExecuteResult) return $handleResult;
            return ExecuteResult::success($handleResult);
        }
        return ExecuteResult::success();
    }
}